#include "test_udp.h"

#include "lwip/udp.h"
#include "lwip/stats.h"
#include "lwip/inet_chksum.h"

#if !LWIP_STATS || !UDP_STATS || !MEMP_STATS
#error "This tests needs UDP- and MEMP-statistics enabled"
#endif

struct test_udp_rxdata {
    u32_t rx_cnt;
    u32_t rx_bytes;
    struct udp_pcb* pcb;
};

static struct netif test_netif1, test_netif2;
static ip4_addr_t test_gw1, test_ipaddr1, test_netmask1;
static ip4_addr_t test_gw2, test_ipaddr2, test_netmask2;
static int output_ctr, linkoutput_ctr;

/* Helper functions */
static void
udp_remove_all(void)
{
    struct udp_pcb* pcb = udp_pcbs;
    struct udp_pcb* pcb2;

    while(pcb != NULL) {
        pcb2 = pcb;
        pcb = pcb->next;
        udp_remove(pcb2);
    }
    fail_unless(MEMP_STATS_GET(used, MEMP_UDP_PCB) == 0);
}

static err_t
default_netif_output(struct netif* netif, struct pbuf* p, const ip4_addr_t* ipaddr)
{
    fail_unless((netif == &test_netif1) || (netif == &test_netif2));
    fail_unless(p != NULL);
    fail_unless(ipaddr != NULL);
    output_ctr++;
    return ERR_OK;
}

static err_t
default_netif_linkoutput(struct netif* netif, struct pbuf* p)
{
    fail_unless((netif == &test_netif1) || (netif == &test_netif2));
    fail_unless(p != NULL);
    linkoutput_ctr++;
    return ERR_OK;
}

static err_t
default_netif_init(struct netif* netif)
{
    fail_unless(netif != NULL);
    netif->output = default_netif_output;
    netif->linkoutput = default_netif_linkoutput;
    netif->mtu = 1500;
    netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;
    netif->hwaddr_len = 6;
    return ERR_OK;
}

static void
default_netif_add(void)
{
    struct netif* n;

#if LWIP_HAVE_LOOPIF
    fail_unless(netif_list != NULL); /* the loopif */
    fail_unless(netif_list->next == NULL);
#else
    fail_unless(netif_list == NULL);
#endif
    fail_unless(netif_default == NULL);

    IP4_ADDR(&test_ipaddr1, 192, 168, 0, 1);
    IP4_ADDR(&test_netmask1, 255, 255, 255, 0);
    IP4_ADDR(&test_gw1, 192, 168, 0, 254);
    n = netif_add(&test_netif1, &test_ipaddr1, &test_netmask1,
                  &test_gw1, NULL, default_netif_init, NULL);
    fail_unless(n == &test_netif1);

    IP4_ADDR(&test_ipaddr2, 192, 168, 1, 1);
    IP4_ADDR(&test_netmask2, 255, 255, 255, 0);
    IP4_ADDR(&test_gw2, 192, 168, 1, 254);
    n = netif_add(&test_netif2, &test_ipaddr2, &test_netmask2,
                  &test_gw2, NULL, default_netif_init, NULL);
    fail_unless(n == &test_netif2);

    netif_set_default(&test_netif1);
    netif_set_up(&test_netif1);
    netif_set_up(&test_netif2);
}

static void
default_netif_remove(void)
{
    fail_unless(netif_default == &test_netif1);
    netif_remove(&test_netif1);
    netif_remove(&test_netif2);
    fail_unless(netif_default == NULL);
#if LWIP_HAVE_LOOPIF
    fail_unless(netif_list != NULL); /* the loopif */
    fail_unless(netif_list->next == NULL);
#else
    fail_unless(netif_list == NULL);
#endif
}
/* Setups/teardown functions */

static void
udp_setup(void)
{
    udp_remove_all();
    default_netif_add();
    lwip_check_ensure_no_alloc(SKIP_POOL(MEMP_SYS_TIMEOUT));
}

static void
udp_teardown(void)
{
    udp_remove_all();
    default_netif_remove();
    lwip_check_ensure_no_alloc(SKIP_POOL(MEMP_SYS_TIMEOUT));
}


/* Test functions */

START_TEST(test_udp_new_remove)
{
    struct udp_pcb* pcb;
    LWIP_UNUSED_ARG(_i);

    fail_unless(MEMP_STATS_GET(used, MEMP_UDP_PCB) == 0);

    pcb = udp_new();
    fail_unless(pcb != NULL);
    if (pcb != NULL) {
        fail_unless(MEMP_STATS_GET(used, MEMP_UDP_PCB) == 1);
        udp_remove(pcb);
        fail_unless(MEMP_STATS_GET(used, MEMP_UDP_PCB) == 0);
    }
}
END_TEST

static void test_recv(void* arg, struct udp_pcb* pcb, struct pbuf* p,
                      const ip_addr_t* addr, u16_t port)
{
    struct test_udp_rxdata* ctr = (struct test_udp_rxdata*)arg;

    LWIP_UNUSED_ARG(addr);
    LWIP_UNUSED_ARG(port);

    fail_unless(arg != NULL);
    fail_unless(ctr->pcb == pcb);

    ctr->rx_cnt++;
    ctr->rx_bytes += p->tot_len;

    if (p != NULL) {
        pbuf_free(p);
    }
}

static struct pbuf*
test_udp_create_test_packet(u16_t length, u16_t port, u32_t dst_addr)
{
    err_t err;
    u8_t ret;
    struct udp_hdr* uh;
    struct ip_hdr* ih;
    struct pbuf* p;
    const u8_t test_data[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};

    p = pbuf_alloc(PBUF_TRANSPORT, length, PBUF_POOL);
    fail_unless(p != NULL);
    if (p == NULL) {
        return NULL;
    }
    fail_unless(p->next == NULL);
    err = pbuf_take(p, test_data, length);
    fail_unless(err == ERR_OK);

    /* add UDP header */
    ret = pbuf_add_header(p, sizeof(struct udp_hdr));
    fail_unless(!ret);
    uh = (struct udp_hdr*)p->payload;
    uh->chksum = 0;
    uh->dest = uh->src = lwip_htons(port);
    uh->len = lwip_htons(p->tot_len);
    /* add IPv4 header */
    ret = pbuf_add_header(p, sizeof(struct ip_hdr));
    fail_unless(!ret);
    ih = (struct ip_hdr*)p->payload;
    memset(ih, 0, sizeof(*ih));
    ih->dest.addr = dst_addr;
    ih->_len = lwip_htons(p->tot_len);
    ih->_ttl = 32;
    ih->_proto = IP_PROTO_UDP;
    IPH_VHL_SET(ih, 4, sizeof(struct ip_hdr) / 4);
    IPH_CHKSUM_SET(ih, inet_chksum(ih, sizeof(struct ip_hdr)));
    return p;
}

/* bind 2 pcbs to specific netif IP and test which one gets broadcasts */
START_TEST(test_udp_broadcast_rx_with_2_netifs)
{
    err_t err;
    struct udp_pcb* pcb1, *pcb2;
    const u16_t port = 12345;
    struct test_udp_rxdata ctr1, ctr2;
    struct pbuf* p;
#if SO_REUSE
    struct udp_pcb* pcb_any;
    struct test_udp_rxdata ctr_any;
#endif
    LWIP_UNUSED_ARG(_i);

    pcb1 = udp_new();
    fail_unless(pcb1 != NULL);
    pcb2 = udp_new();
    fail_unless(pcb2 != NULL);

#if SO_REUSE
    pcb_any = udp_new();
    fail_unless(pcb_any != NULL);

    ip_set_option(pcb1, SOF_REUSEADDR);
    ip_set_option(pcb2, SOF_REUSEADDR);
    ip_set_option(pcb_any, SOF_REUSEADDR);

    err = udp_bind(pcb_any, NULL, port);
    fail_unless(err == ERR_OK);
    memset(&ctr_any, 0, sizeof(ctr_any));
    ctr_any.pcb = pcb_any;
    udp_recv(pcb_any, test_recv, &ctr_any);
#endif

    err = udp_bind(pcb1, &test_netif1.ip_addr, port);
    fail_unless(err == ERR_OK);
    err = udp_bind(pcb2, &test_netif2.ip_addr, port);
    fail_unless(err == ERR_OK);

    memset(&ctr1, 0, sizeof(ctr1));
    ctr1.pcb = pcb1;
    memset(&ctr2, 0, sizeof(ctr2));
    ctr2.pcb = pcb2;

    udp_recv(pcb1, test_recv, &ctr1);
    udp_recv(pcb2, test_recv, &ctr2);

    /* unicast to netif1 */
    p = test_udp_create_test_packet(16, port, test_ipaddr1.addr);
    EXPECT_RET(p != NULL);
    err = ip4_input(p, &test_netif1);
    fail_unless(err == ERR_OK);
    fail_unless(ctr1.rx_cnt == 1);
    fail_unless(ctr1.rx_bytes == 16);
    fail_unless(ctr2.rx_cnt == 0);
#if SO_REUSE
    fail_unless(ctr_any.rx_cnt == 0);
#endif
    ctr1.rx_cnt = ctr1.rx_bytes = 0;

    /* unicast to netif2 */
    p = test_udp_create_test_packet(16, port, test_ipaddr2.addr);
    EXPECT_RET(p != NULL);
    err = ip4_input(p, &test_netif2);
    fail_unless(err == ERR_OK);
    fail_unless(ctr2.rx_cnt == 1);
    fail_unless(ctr2.rx_bytes == 16);
    fail_unless(ctr1.rx_cnt == 0);
#if SO_REUSE
    fail_unless(ctr_any.rx_cnt == 0);
#endif
    ctr2.rx_cnt = ctr2.rx_bytes = 0;

    /* broadcast to netif1-broadcast, input to netif2 */
    p = test_udp_create_test_packet(16, port, test_ipaddr1.addr | ~test_netmask1.addr);
    EXPECT_RET(p != NULL);
    err = ip4_input(p, &test_netif2);
    fail_unless(err == ERR_OK);
    fail_unless(ctr1.rx_cnt == 1);
    fail_unless(ctr1.rx_bytes == 16);
    fail_unless(ctr2.rx_cnt == 0);
#if SO_REUSE
    fail_unless(ctr_any.rx_cnt == 0);
#endif
    ctr1.rx_cnt = ctr1.rx_bytes = 0;

    /* broadcast to netif2-broadcast, input to netif1 */
    p = test_udp_create_test_packet(16, port, test_ipaddr2.addr | ~test_netmask2.addr);
    EXPECT_RET(p != NULL);
    err = ip4_input(p, &test_netif1);
    fail_unless(err == ERR_OK);
    fail_unless(ctr2.rx_cnt == 1);
    fail_unless(ctr2.rx_bytes == 16);
    fail_unless(ctr1.rx_cnt == 0);
#if SO_REUSE
    fail_unless(ctr_any.rx_cnt == 0);
#endif
    ctr2.rx_cnt = ctr2.rx_bytes = 0;

    /* broadcast to global-broadcast, input to netif1 */
    p = test_udp_create_test_packet(16, port, 0xffffffff);
    EXPECT_RET(p != NULL);
    err = ip4_input(p, &test_netif1);
    fail_unless(err == ERR_OK);
    fail_unless(ctr1.rx_cnt == 1);
    fail_unless(ctr1.rx_bytes == 16);
    fail_unless(ctr2.rx_cnt == 0);
#if SO_REUSE
    fail_unless(ctr_any.rx_cnt == 0);
#endif
    ctr1.rx_cnt = ctr1.rx_bytes = 0;

    /* broadcast to global-broadcast, input to netif2 */
    p = test_udp_create_test_packet(16, port, 0xffffffff);
    EXPECT_RET(p != NULL);
    err = ip4_input(p, &test_netif2);
    fail_unless(err == ERR_OK);
    fail_unless(ctr2.rx_cnt == 1);
    fail_unless(ctr2.rx_bytes == 16);
    fail_unless(ctr1.rx_cnt == 0);
#if SO_REUSE
    fail_unless(ctr_any.rx_cnt == 0);
#endif
    ctr2.rx_cnt = ctr2.rx_bytes = 0;
}
END_TEST

/** Create the suite including all tests for this module */
Suite*
udp_suite(void)
{
    testfunc tests[] = {
        TESTFUNC(test_udp_new_remove),
        TESTFUNC(test_udp_broadcast_rx_with_2_netifs)
    };
    return create_suite("UDP", tests, sizeof(tests) / sizeof(testfunc), udp_setup, udp_teardown);
}
